﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Ccr.Core;
using Microsoft.Ccr.Core.Arbiters;
using XcoAppSpaces.Contracts.CcrExtensions;
using XcoAppSpaces.Contracts.CcrWiring;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Service;

namespace XcoAppSpaces.Core.CcrWiring
{
	/// <summary>
	/// The WorkerFactory class is responsible for creating workers and wiring their ports to the appropriate
	/// message handlers.
	/// </summary>
	class WorkerFactory
	{
		public WorkerInfo<TMessageContract> Create<TMessageContract, TImplementation>(DispatcherQueue queue)
			where TMessageContract : IPort
			where TImplementation : TMessageContract, new()
		{
			return Wire<TMessageContract>(new TImplementation(), queue);
		}

		public WorkerInfo<TMessageContract> Wire<TMessageContract>(TMessageContract worker, DispatcherQueue queue)
			where TMessageContract : IPort
		{
			var messageContractPorts = GetMessageContractPorts(worker);
			var messageProcessors = GetMessageProcessorMethods(worker);
			var wiredProcessors = WirePortsToProcessorMethods(messageContractPorts, messageProcessors, queue);

			return new WorkerInfo<TMessageContract>(worker,
				GetServiceAttributes(worker, messageProcessors),
				wiredProcessors.ToDictionary(k => k.Key, v => v.Value.ProcessorAttr));
		}

		internal Dictionary<Type, IPort> GetMessageContractPorts(IPort messageContract)
		{
			var messageContractPorts = new Dictionary<Type, IPort>();
			if (messageContract is IPortSet)
			{
				foreach (var port in ((IPortSet)messageContract).Ports)
				{
					messageContractPorts.Add(port.GetMessageType(), port);
				}
			}
			else
			{
				messageContractPorts.Add(messageContract.GetMessageType(), messageContract);
			}
			return messageContractPorts;
		}

		internal List<MessageProcessorInfo> GetMessageProcessorMethods(object worker)
		{
			var processorMethods = CompileMessageProcessorMethodsFromType(worker, null).ToList();
			foreach (var workerExtension in GetWorkerExtensions(worker))
			{
				processorMethods.AddRange(CompileMessageProcessorMethodsFromType(workerExtension.Obj, workerExtension));
			}
			return processorMethods;
		}

		internal IEnumerable<MessageProcessorInfo> CompileMessageProcessorMethodsFromType(object worker, WorkerExtensionInfo workerOptions)
		{
			return from m in worker.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
				   let attrs = m.GetCustomAttributes(typeof(XcoProcessorAttribute), true)
				   where attrs.Length > 0
				   select (new MessageProcessorInfo(worker, m, (XcoProcessorAttribute)attrs[0], GetServiceAttributes(m), workerOptions));
		}

		internal IEnumerable<WorkerExtensionInfo> GetWorkerExtensions(object worker)
		{
			foreach (var ext in worker.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
				.Select(f => new { Field = f, Attr = GetWorkerExtensionAttribute(f) })
				.Where(f => f.Attr != null && f.Field.GetValue(worker) != null)
				.Select(f => new WorkerExtensionInfo(f.Field.GetValue(worker), f.Attr, GetServiceAttributes(f.Field))))
			{
				yield return ext;
				foreach (var nestedExt in GetNestedWorkerExtensions(ext.Obj))
					yield return new WorkerExtensionInfo(nestedExt, ext.ExtensionAttr, ext.ServiceAttrs);
			}
		}

		internal IEnumerable<object> GetNestedWorkerExtensions(object workerExtension)
		{
			foreach (var ext in workerExtension.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
				.Where(f => GetWorkerExtensionAttribute(f) != null)
				.Select(f => f.GetValue(workerExtension))
				.Where(v => v != null))
			{
				yield return ext;
				foreach (var nestedExt in GetNestedWorkerExtensions(ext))
					yield return nestedExt;
			}
		}

		internal XcoWorkerExtensionAttribute GetWorkerExtensionAttribute(FieldInfo field)
		{
			var fieldAttrs = field.GetCustomAttributes(typeof(XcoWorkerExtensionAttribute), false);
			var classAttrs = field.FieldType.GetCustomAttributes(typeof(XcoWorkerExtensionAttribute), false);
			if (fieldAttrs.Length > 0 && classAttrs.Length == 0)
				throw new XcoWorkerException("A field can only be marked with the XcoWorkerExtension attribute when its type is also marked with this attribute");

			if (fieldAttrs.Length > 0)
				return (XcoWorkerExtensionAttribute)fieldAttrs[0];
			if (classAttrs.Length > 0)
				return (XcoWorkerExtensionAttribute)classAttrs[0];
			return null;
		}

		internal Dictionary<Type, MessageProcessorInfo> WirePortsToProcessorMethods(Dictionary<Type, IPort> messageContractPorts, IEnumerable<MessageProcessorInfo> messageProcessors, DispatcherQueue queue)
		{
			var wiredProcessors = new Dictionary<Type, MessageProcessorInfo>();
			var exclusiveTasks = CreateCcrReceiversForProcessorMethods(messageContractPorts, messageProcessors.Where(p => p.ProcessorAttr is XcoExclusiveAttribute), queue, true, wiredProcessors);
			var concurrentTasks = CreateCcrReceiversForProcessorMethods(messageContractPorts, messageProcessors.Where(p => p.ProcessorAttr is XcoConcurrentAttribute), queue, true, wiredProcessors);
			var teardownTasks = CreateCcrReceiversForProcessorMethods(messageContractPorts, messageProcessors.Where(p => p.ProcessorAttr is XcoTeardownAttribute), queue, false, wiredProcessors);
			Arbiter.Activate(
				queue,
				Arbiter.Interleave(
					new TeardownReceiverGroup(teardownTasks),
					new ExclusiveReceiverGroup(exclusiveTasks),
					new ConcurrentReceiverGroup(concurrentTasks)
					)
				);
			return wiredProcessors;
		}

		internal ReceiverTask[] CreateCcrReceiversForProcessorMethods(Dictionary<Type, IPort> messageContractPorts, IEnumerable<MessageProcessorInfo> messageProcessors, DispatcherQueue queue, bool makePersistent, Dictionary<Type, MessageProcessorInfo> alreadyWiredProcessors)
		{
			var tasks = new List<ReceiverTask>();
			foreach (var mi in messageProcessors)
			{
				if (mi.Method.GetParameters().Length != 1)
					throw new XcoWorkerException("Message Processor method " + mi.Method.Name + " has an invalid number of parameters! A message processor method must have only a single parameter, which corresponds to the type of a worker port.");
				var messageProcessingTask =
					CreateCcrReceiverToBindProcessorMethodToPort(messageContractPorts, mi, queue, makePersistent, alreadyWiredProcessors);
				if (messageProcessingTask != null)
				{
					tasks.Add(messageProcessingTask);
				}
			}
			return tasks.ToArray();
		}


		internal ReceiverTask CreateCcrReceiverToBindProcessorMethodToPort(Dictionary<Type, IPort> messageContractPorts, MessageProcessorInfo messageProcessor, DispatcherQueue queue, bool makePersistent, Dictionary<Type, MessageProcessorInfo> alreadyWiredProcessors)
		{
			var messageType = GetMessageTypeForProcessorMethod(messageProcessor.Method);
			if (alreadyWiredProcessors.ContainsKey(messageType))
			{
				if (messageProcessor.WorkerOptions == null || messageProcessor.WorkerOptions.IsTypeRequired(messageType))
					throw new XcoWorkerException("Duplicate processor method found for type " + messageType + ". There can only be one required processor method per type!");
				return null;
			}

			var messagePort = GetPortForMessageType(messageContractPorts, messageType);
			if (messagePort != null)
			{
				var task = CreateCcrTaskToBindProcessorMethodToPort(messageType, messageProcessor);
				alreadyWiredProcessors.Add(messageType, messageProcessor);
				return CreateCcrReceiverForMessageType(messageType, messagePort, task, makePersistent);
			}

			if (messageProcessor.WorkerOptions == null || messageProcessor.WorkerOptions.IsTypeRequired(messageType))
				throw new XcoWorkerException("No port found for required message type " + messageType);
			return null;
		}


		internal Type GetMessageTypeForProcessorMethod(MethodInfo messageProcessor)
		{
			return messageProcessor.GetParameters()[0].ParameterType;
		}

		internal IPort GetPortForMessageType(Dictionary<Type, IPort> messageContractPorts, Type messageType)
		{
			if (messageContractPorts.ContainsKey(messageType))
			{
				var port = messageContractPorts[messageType];
				messageContractPorts.Remove(messageType); //remove port from list of available ports
				return port;
			}
			return null;
		}

		internal ITask CreateCcrTaskToBindProcessorMethodToPort(Type messageType, MessageProcessorInfo messageProcessor)
		{
			if (messageProcessor.Method.ReturnType == typeof(IEnumerator<ITask>))
				return CreateCcrIterativeTaskToBindProcessorMethodToPort(messageType, messageProcessor);
			if (messageProcessor.Method.ReturnType == typeof(void))
				return CreateCcrStandardTaskToBindProcessorMethodToPort(messageType, messageProcessor);
			throw new XcoWorkerException("Return type of a message processor method can only be void or IEnumerator<ITask>!");
		}

		internal ITask CreateCcrStandardTaskToBindProcessorMethodToPort(Type messageType, MessageProcessorInfo messageProcessor)
		{
			Type handlerType = typeof(Handler<>).MakeGenericType(messageType);
			Delegate handler = Delegate.CreateDelegate(handlerType, messageProcessor.MethodHost, messageProcessor.Method);

			Type taskType = typeof(Task<>).MakeGenericType(messageType);
			return (ITask)taskType.GetConstructor(new[] { handlerType }).Invoke(new object[] { handler });
		}

		internal ITask CreateCcrIterativeTaskToBindProcessorMethodToPort(Type messageType, MessageProcessorInfo iterativeMessageProcessor)
		{
			Type handlerType = typeof(IteratorHandler<>).MakeGenericType(messageType);
			Delegate handler = Delegate.CreateDelegate(handlerType, iterativeMessageProcessor.MethodHost, iterativeMessageProcessor.Method);

			Type taskType = typeof(IterativeTask<>).MakeGenericType(messageType);
			return (ITask)taskType.GetConstructor(new[] { handlerType }).Invoke(new object[] { handler });
		}

		internal ReceiverTask CreateCcrReceiverForMessageType(Type messageType, IPort messagePort, ITask task, bool makePersistent)
		{
			Type receiverType = typeof(Receiver<>).MakeGenericType(messageType);
			return (ReceiverTask)receiverType
				.GetConstructor(new[] { typeof(bool), messagePort.GetType(), typeof(Predicate<>).MakeGenericType(messageType), task.GetType() })
				.Invoke(new object[] { makePersistent, messagePort, null, task });
		}

		internal List<XcoServiceAttributeInfo> GetServiceAttributes(object worker, params List<MessageProcessorInfo>[] messageProcessors)
		{
			var attrs = new List<XcoServiceAttributeInfo>();
			foreach (var list in messageProcessors)
			{
				foreach (var info in list)
				{
					attrs.AddRange(info.ServiceAttrs.Select(attr => new XcoServiceAttributeInfo { Attr = attr, WorkerMessageType = info.Method.GetParameters()[0].ParameterType }));
					//attrs.AddRange(
					//	info.Method.GetCustomAttributes(typeof(XcoServiceAttribute), true)
					//	.Select(attr => new XcoServiceAttributeInfo { Attr = (XcoServiceAttribute)attr, WorkerMessageType = info.Method.GetParameters()[0].ParameterType }));
				}
			}
			attrs.AddRange(
						worker.GetType().GetCustomAttributes(typeof(XcoServiceAttribute), true)
						.Select(attr => new XcoServiceAttributeInfo { Attr = (XcoServiceAttribute)attr, WorkerMessageType = null }));
			return attrs;
		}

		internal XcoServiceAttribute[] GetServiceAttributes(MemberInfo info)
		{
			return info.GetCustomAttributes(typeof (XcoServiceAttribute), true).Cast<XcoServiceAttribute>().ToArray();
		}

		internal class WorkerExtensionInfo
		{
			public object Obj { get; private set; }
			public XcoWorkerExtensionAttribute ExtensionAttr { get; private set; }
			public XcoServiceAttribute[] ServiceAttrs { get; private set; }


			public WorkerExtensionInfo(object obj, XcoWorkerExtensionAttribute extensionAttr, XcoServiceAttribute[] serviceAttrs)
			{
				this.Obj = obj;
				this.ExtensionAttr = extensionAttr;
				this.ServiceAttrs = serviceAttrs;
			}
		}

		internal class MessageProcessorInfo
		{
			public object MethodHost { get; private set; }
			public MethodInfo Method { get; private set; }
			public XcoProcessorAttribute ProcessorAttr { get; private set; }
			public XcoServiceAttribute[] ServiceAttrs { get; private set; }
			public XcoWorkerExtensionAttribute WorkerOptions { get; private set; }
			
			public MessageProcessorInfo(object methodHost, MethodInfo method, XcoProcessorAttribute processorAttr, XcoServiceAttribute[] serviceAttrs, WorkerExtensionInfo workerOptions)
			{
				this.MethodHost = methodHost;
				this.Method = method;
				this.ProcessorAttr = processorAttr;
				this.ServiceAttrs = serviceAttrs;
				if (workerOptions != null)
				{
					this.WorkerOptions = workerOptions.ExtensionAttr;
					this.ServiceAttrs = serviceAttrs.Union(workerOptions.ServiceAttrs).ToArray();
				}
			}
		}
	}
}
